package com.gitee.cashzhang27.test.boot.crypt.mapper.interceptor;

import cn.hutool.crypto.SecureUtil;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.CreateTime;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.CreateUser;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.DecryptFiled;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.EncryptFiled;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.LogicDelete;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.ModifiedTime;
import com.gitee.cashzhang27.test.boot.crypt.mapper.annotation.ModifiedUser;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.persistence.Column;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;

/**
 * @author Cash Zhang
 * @version v1.0
 * @since 2019/07/03 11:27
 */
@Slf4j
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
        Integer.class}),
    @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Component
public class MybatisCryptoIntercept implements Interceptor {

  private String keyword = "123";

  private final static String WHERE = "WHERE";

  private final static String SELECTIVE = "Selective";

  private final static String DELETE = "delete";

  private final static String SPLIT = "? )";

  private final static String BASE_MAPPER = "com.gitee.cashzhang27.test.boot.crypt.mapper.util.CrudMapper";

  private final List<String> interceptorMethodList = Arrays.asList(
      "insert", "insertSelective",
      "delete", "deleteByPrimaryKey",
      "updateByPrimaryKey", "updateByPrimaryKeySelective",
      "select", "selectOne", "selectByPrimaryKey", "selectByIds", "selectAll");

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();

    if (target instanceof StatementHandler) {
      StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
      // 拿到target的元数据
      MetaObject metaObject = SystemMetaObject.forObject(target);

      Object parameterObject = metaObject.getValue("parameterHandler.parameterObject");
      log.debug("sql语句用的参数是：" + parameterObject);

      MappedStatement mappedStatement = (MappedStatement) metaObject
          .getValue("delegate.mappedStatement");
      // 获取SQL命令类型
      SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();

      BoundSql boundSql = statementHandler.getBoundSql();
      //获取到原始sql语句
      String originalSql = boundSql.getSql();
      // 获取 mapper 方法名
      String namespace = mappedStatement.getId();
      String mapperMethodName = namespace.substring(namespace.lastIndexOf(".") + 1);
      // 得到实体类名
      String entityClassName = validateIntercept(namespace);
      //通过反射修改sql语句
      Field sqlField = boundSql.getClass().getDeclaredField("sql");
      if (StringUtils.isBlank(originalSql) || StringUtils.isBlank(entityClassName)) {
        return invocation.proceed();
      }
      if (SqlCommandType.INSERT.equals(sqlCommandType)) {
        String newSql = insertProcess(parameterObject, mapperMethodName, originalSql);
        if (StringUtils.isNotBlank(newSql)) {
          sqlField.setAccessible(true);
          sqlField.set(boundSql, newSql);
        }
        return invocation.proceed();
      } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
        String newSql = updateProcess(parameterObject, mapperMethodName, originalSql);
        if (StringUtils.isNotBlank(newSql)) {
          sqlField.setAccessible(true);
          sqlField.set(boundSql, newSql);
        }
      } else if (SqlCommandType.SELECT.equals(sqlCommandType)) {
        selectProcess(parameterObject);
      }

      Map<String, String> map = hasLogicDelete(entityClassName);
      String newSql;
      originalSql = boundSql.getSql();
      if (map.size() > 0) {
        String columnName = map.get("columnName");
        if (originalSql.contains(WHERE) || originalSql.contains(WHERE.toLowerCase())) {
          newSql = originalSql + " AND " + columnName + " = 0 ";
        } else {
          newSql = originalSql + " WHERE " + columnName + " = 0 ";
        }
        if (SqlCommandType.DELETE.equals(sqlCommandType)) {
          newSql = deleteProcess(parameterObject, mapperMethodName, originalSql, columnName);
        }
        sqlField.setAccessible(true);
        sqlField.set(boundSql, newSql);
      }
      metaObject.setValue("parameterHandler.parameterObject", parameterObject);
    }
    if (target instanceof ResultSetHandler) {
      Object result = invocation.proceed();
      return postSelect(result);
    }
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
  }

  private String validateIntercept(String namespace) throws ClassNotFoundException {
    // 获取 mapper 方法名
    String mapperMethodName = namespace.substring(namespace.lastIndexOf(".") + 1);
    // 获取 mapper class
    Class<?> mapperClazz = Class.forName(namespace.replace("." + mapperMethodName, ""));
    Type[] genericInterfaces = mapperClazz.getGenericInterfaces();
    if (genericInterfaces.length > 0) {
      // 获取 mapper 父接口
      Type superInterfaces = genericInterfaces[0];
      // 判断是否继承 BaseMapper 且 使用的是拦截方法
      if (superInterfaces.getTypeName().contains(BASE_MAPPER)
          && interceptorMethodList.contains(mapperMethodName)) {
        // 得到实际的泛型
        Type type = ((ParameterizedType) superInterfaces).getActualTypeArguments()[0];
        return type.getTypeName();
      }
    }
    return null;
  }

  private byte[] generateKey(String keyword) throws NoSuchAlgorithmException {
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    kgen.init(128, new SecureRandom(keyword.getBytes()));
    SecretKey secretKey = kgen.generateKey();
    return secretKey.getEncoded();
  }


  private String insertProcess(Object parameterObject, String mapperMethodName, String originalSql)
      throws Exception {
    Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
    String newSql = originalSql;
    for (Field field : declaredFields) {
      if (field.getAnnotation(CreateTime.class) != null) {
        if (mapperMethodName.contains(SELECTIVE)) {
          newSql = getInsertSql(field,
              LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
              newSql);
        } else {
          field.setAccessible(true);
          field.set(parameterObject, LocalDateTime.now());
        }
      }
      if (field.getAnnotation(CreateUser.class) != null) {
        if (mapperMethodName.contains(SELECTIVE)) {
          newSql = getInsertSql(field, "大娃", newSql);
        } else {
          field.setAccessible(true);
          field.set(parameterObject, "大娃");
        }
      }
      encryptBase64Value(parameterObject, field);
    }
    return newSql;
  }


  private String deleteProcess(Object parameterObject, String mapperMethodName, String originalSql,
      String columnName) throws Exception {

    if (Objects.equals(mapperMethodName, DELETE)) {
      Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
      for (Field field : declaredFields) {
        encryptBase64Value(parameterObject, field);
      }
    }
    return originalSql
        .replace("DELETE FROM", "UPDATE")
        .replace("WHERE", "SET " + columnName + " = 1 WHERE");
  }

  private String updateProcess(Object parameterObject, String mapperMethodName, String originalSql)
      throws Exception {
    Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
    String newSql = originalSql;
    for (Field field : declaredFields) {
      if (field.getAnnotation(ModifiedTime.class) != null) {
        if (mapperMethodName.contains(SELECTIVE)) {
          newSql = getUpdateSql(field,
              LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
              newSql);
        } else {
          field.setAccessible(true);
          field.set(parameterObject, LocalDateTime.now());
        }
      }
      if (field.getAnnotation(ModifiedUser.class) != null) {
        if (mapperMethodName.contains(SELECTIVE)) {
          newSql = getUpdateSql(field, "二娃", newSql);
        } else {
          field.setAccessible(true);
          field.set(parameterObject, "二娃");
        }
      }
      encryptBase64Value(parameterObject, field);
    }
    return newSql;
  }

  private void selectProcess(Object parameterObject)
      throws Exception {
    if (Objects.nonNull(parameterObject)) {
      Field[] declaredFields = parameterObject.getClass().getDeclaredFields();
      for (Field field : declaredFields) {
        encryptBase64Value(parameterObject, field);
      }
    }
  }

  private void encryptBase64Value(Object parameterObject, Field field)
      throws Exception {
    if (field.getAnnotation(EncryptFiled.class) != null) {
      // 生成密钥
      byte[] key = generateKey(keyword);
      Object fieldValue = getFieldValue(parameterObject, field);
      if (Objects.nonNull(fieldValue)) {
        field.setAccessible(true);
        log.debug("加密前：" + field.getName() + "-->" + fieldValue);
        String encryptBase64Value = SecureUtil.aes(key)
            .encryptBase64(String.valueOf(fieldValue));
        log.debug("加密后：" + field.getName() + "-->" + encryptBase64Value);
        field.set(parameterObject, encryptBase64Value);
      }
    }
  }

  private String getInsertSql(Field field, Object value, String originalSql) {
    String columnName = getColumnName(field);
    String newSql = originalSql
        .replace(" ) VALUES( ",
            "," + columnName + " ) VALUES( ?,");
    if (newSql.contains(SPLIT)) {
      newSql = newSql.replace("? )",
          "'" + value + "')");
    } else {
      newSql = newSql.replace(") VALUES( ?,", ") VALUES(");
      newSql = newSql.substring(0, newSql.length() - 1) + ",'" + value + "')";
    }
    return newSql;
  }

  private String getUpdateSql(Field field, Object value, String originalSql) {
    String columnName = getColumnName(field);
    return originalSql
        .replace("SET", "SET " + columnName + "='" + value + "',");
  }

  private String getColumnName(Field field) {
    Column column = field.getAnnotation(Column.class);
    String columnName;
    if (column != null) {
      columnName = column.name();
    } else {
      columnName = humpToUnderline(field.getName());
    }
    return columnName;
  }

  private Object postSelect(Object result) throws Exception {
    if (result instanceof ArrayList) {
      ArrayList resultList = (ArrayList) result;
      for (Object obj : resultList) {
        // 获取实体类的所有属性，返回Field数组
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
          if (field.getAnnotation(DecryptFiled.class) != null) {
            // 生成密钥
            byte[] key = generateKey(keyword);
            Object fieldValue = getFieldValue(obj, field);
            if (Objects.nonNull(fieldValue)) {
              // 可访问私有变量
              field.setAccessible(true);
              log.debug("解密前：" + field.getName() + "-->" + fieldValue);
              String decodeValue = SecureUtil.aes(key)
                  .decryptStr(Base64Utils
                      .decodeFromString(String.valueOf(getFieldValue(obj, field))));
              log.debug("解密后：" + field.getName() + "-->" + decodeValue);
              field.set(obj, decodeValue);
            }
          }
        }
      }
    }
    return result;
  }

  private Object getFieldValue(Object obj, Field field)
      throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
    String fieldName = field.getName();
    // 将属性的首字母大写
    String getMethodName = "get" + fieldName
        .replaceFirst(fieldName.substring(0, 1),
            fieldName.substring(0, 1).toUpperCase());
    Method m = obj.getClass().getMethod(getMethodName);
    return m.invoke(obj);
  }

  private Map<String, String> hasLogicDelete(String entityClassName) throws ClassNotFoundException {
    Map<String, String> map = new HashMap<>(1 << 4);
    Class<?> entityClazz = Class.forName(entityClassName);
    // 获取私有属性，并判断是否含有 LogicDelete 注解
    Field[] entityFields = entityClazz.getDeclaredFields();
    for (Field field : entityFields) {
      if (field.getAnnotation(LogicDelete.class) != null) {
        Column column = field.getAnnotation(Column.class);
        if (column != null) {
          map.put("columnName", column.name());
        } else {
          map.put("columnName", humpToUnderline(field.getName()));
        }
        break;
      }
    }
    return map;
  }

  /***
   * 驼峰命名转为下划线命名
   *
   * @param para
   *        驼峰命名的字符串
   */
  private String humpToUnderline(String para) {
    StringBuilder sb = new StringBuilder(para);
    //定位
    int temp = 0;
    for (int i = 0; i < para.length(); i++) {
      if (Character.isUpperCase(para.charAt(i))) {
        sb.insert(i + temp, "_");
        temp += 1;
      }
    }
    return sb.toString().toLowerCase();
  }

}